import crypto from 'crypto'
import _ from 'lodash'
import path from 'path'
import fsAsync from 'fs/promises'
import getLambdaLayerArtifactPath from '../../utils/get-lambda-layer-artifact-path.js'
import utils from '@serverlessinc/sf-core/src/utils.js'

const { log } = utils

class AwsCompileLayers {
  constructor(serverless, options) {
    this.serverless = serverless
    this.options = options
    const serviceDir = this.serverless.serviceDir || ''
    this.packagePath =
      this.serverless.service.package.path ||
      path.join(serviceDir || '.', '.serverless')

    this.provider = this.serverless.getProvider('aws')

    this.hooks = {
      'package:compileLayers': async () => this.compileLayers(),
    }
  }

  async compileLayer(layerName) {
    const newLayer = this.cfLambdaLayerTemplate()
    const layerObject = this.serverless.service.getLayer(layerName)
    layerObject.package = layerObject.package || {}
    Object.defineProperty(newLayer, '_serverlessLayerName', {
      value: layerName,
    })

    const artifactFilePath = this.provider.resolveLayerArtifactName(layerName)

    if (this.serverless.service.package.deploymentBucket) {
      newLayer.Properties.Content.S3Bucket =
        this.serverless.service.package.deploymentBucket
    }

    const s3Folder = this.serverless.service.package.artifactDirectoryName
    const s3FileName = artifactFilePath.split(path.sep).pop()
    newLayer.Properties.Content.S3Key = `${s3Folder}/${s3FileName}`

    newLayer.Properties.LayerName = layerObject.name || layerName
    if (layerObject.description) {
      newLayer.Properties.Description = layerObject.description
    }
    if (layerObject.licenseInfo) {
      newLayer.Properties.LicenseInfo = layerObject.licenseInfo
    }
    if (layerObject.compatibleRuntimes) {
      newLayer.Properties.CompatibleRuntimes = layerObject.compatibleRuntimes
    }
    if (layerObject.compatibleArchitectures) {
      newLayer.Properties.CompatibleArchitectures =
        layerObject.compatibleArchitectures
    }

    let layerLogicalId = this.provider.naming.getLambdaLayerLogicalId(layerName)
    const layerArtifactPath = getLambdaLayerArtifactPath(
      this.packagePath,
      this.serverless.serviceDir,
      layerName,
      this.provider.serverless.service,
      this.provider.naming,
    )
    return fsAsync.readFile(layerArtifactPath).then((layerArtifactBinary) => {
      const sha = crypto
        .createHash('sha1')
        .update(JSON.stringify(_.omit(newLayer, ['Properties.Content.S3Key'])))
        .update(layerArtifactBinary)
        .digest('hex')
      if (layerObject.retain) {
        layerLogicalId = `${layerLogicalId}${sha}`
        newLayer.DeletionPolicy = 'Retain'
      }
      const newLayerObject = {
        [layerLogicalId]: newLayer,
      }

      if (layerObject.allowedAccounts) {
        layerObject.allowedAccounts.map((account) => {
          const newPermission = this.cfLambdaLayerPermissionTemplate()
          newPermission.Properties.LayerVersionArn = { Ref: layerLogicalId }
          newPermission.Properties.Principal = account
          let layerPermLogicalId =
            this.provider.naming.getLambdaLayerPermissionLogicalId(
              layerName,
              account,
            )
          if (layerObject.retain) {
            layerPermLogicalId = `${layerPermLogicalId}${sha}`
            newPermission.DeletionPolicy = 'Retain'
          }
          newLayerObject[layerPermLogicalId] = newPermission
          return newPermission
        })
      }

      Object.assign(
        this.serverless.service.provider.compiledCloudFormationTemplate
          .Resources,
        newLayerObject,
      )

      // Add layer to Outputs section
      const layerOutputLogicalId =
        this.provider.naming.getLambdaLayerOutputLogicalId(layerName)
      const newLayerOutput = this.cfOutputLayerTemplate()

      newLayerOutput.Value = { Ref: layerLogicalId }

      const layerHashOutputLogicalId =
        this.provider.naming.getLambdaLayerHashOutputLogicalId(layerName)
      const newLayerHashOutput = this.cfOutputLayerHashTemplate()
      newLayerHashOutput.Value = sha

      const layerS3KeyOutputLogicalId =
        this.provider.naming.getLambdaLayerS3KeyOutputLogicalId(layerName)
      const newLayerS3KeyOutput = this.cfOutputLayerS3KeyTemplate()
      newLayerS3KeyOutput.Value = newLayer.Properties.Content.S3Key

      _.merge(
        this.serverless.service.provider.compiledCloudFormationTemplate.Outputs,
        {
          [layerOutputLogicalId]: newLayerOutput,
          [layerHashOutputLogicalId]: newLayerHashOutput,
          [layerS3KeyOutputLogicalId]: newLayerS3KeyOutput,
        },
      )
    })
  }

  async compareWithLastLayer(layerName) {
    const stackName = this.provider.naming.getStackName()
    const layerHashOutputLogicalId =
      this.provider.naming.getLambdaLayerHashOutputLogicalId(layerName)

    return this.provider
      .request('CloudFormation', 'describeStacks', { StackName: stackName })
      .then(
        (data) => {
          const lastHash = data.Stacks[0].Outputs.find(
            (output) => output.OutputKey === layerHashOutputLogicalId,
          )
          const compiledCloudFormationTemplate =
            this.serverless.service.provider.compiledCloudFormationTemplate
          const newSha =
            compiledCloudFormationTemplate.Outputs[layerHashOutputLogicalId]
              .Value
          if (lastHash == null || lastHash.OutputValue !== newSha) {
            return
          }

          const layerS3keyOutputLogicalId =
            this.provider.naming.getLambdaLayerS3KeyOutputLogicalId(layerName)
          const lastS3Key = data.Stacks[0].Outputs.find(
            (output) => output.OutputKey === layerS3keyOutputLogicalId,
          )
          compiledCloudFormationTemplate.Outputs[
            layerS3keyOutputLogicalId
          ].Value = lastS3Key.OutputValue
          const layerLogicalId =
            this.provider.naming.getLambdaLayerLogicalId(layerName)
          const layerResource =
            compiledCloudFormationTemplate.Resources[layerLogicalId] ||
            compiledCloudFormationTemplate.Resources[
              `${layerLogicalId}${lastHash.OutputValue}`
            ]
          layerResource.Properties.Content.S3Key = lastS3Key.OutputValue
          const layerObject = this.serverless.service.getLayer(layerName)
          layerObject.artifactAlreadyUploaded = true
          log.info(`Layer ${layerName} is already uploaded.`)
        },
        (e) => {
          if (e.message.includes('does not exist')) {
            return
          }
          throw e
        },
      )
  }

  async compileLayers() {
    const allLayers = this.serverless.service.getAllLayers()
    await Promise.all(
      allLayers.map((layerName) =>
        this.compileLayer(layerName).then(() =>
          this.compareWithLastLayer(layerName),
        ),
      ),
    )
  }

  cfLambdaLayerTemplate() {
    return {
      Type: 'AWS::Lambda::LayerVersion',
      Properties: {
        Content: {
          S3Bucket: {
            Ref: 'ServerlessDeploymentBucket',
          },
          S3Key: 'S3Key',
        },
        LayerName: 'LayerName',
      },
    }
  }

  cfLambdaLayerPermissionTemplate() {
    return {
      Type: 'AWS::Lambda::LayerVersionPermission',
      Properties: {
        Action: 'lambda:GetLayerVersion',
        LayerVersionArn: 'LayerVersionArn',
        Principal: 'Principal',
      },
    }
  }

  cfOutputLayerTemplate() {
    return {
      Description: 'Current Lambda layer version',
      Value: 'Value',
    }
  }

  cfOutputLayerHashTemplate() {
    return {
      Description: 'Current Lambda layer hash',
      Value: 'Value',
    }
  }

  cfOutputLayerS3KeyTemplate() {
    return {
      Description: 'Current Lambda layer S3Key',
      Value: 'Value',
    }
  }
}

export default AwsCompileLayers
